﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

using QuickGraph;
using System.Configuration;
using Microsoft.Xrm.Sdk;

namespace CrmWorkflowDependencies.GraphBuilding
{
    using Dependencies;
    using Crm;
    using Crm.Extension;

    public class Program
    {
        const string _paramAnchorEntityName = "etn";
        const string _paramOutputFileName = "ofn";

        static IOrganizationService crm;

        static Program()
        {
            DependenciesException.ThrowIf(ConfigurationManager.ConnectionStrings.Count == 0, "there must be at least one connection string in configuration file named CRM_SERVICE");
            crm = XrmService.GetOrganizationProxy("CRM_SERVICE");
        }

        static void Main(string[] args)
        {
            try
            {
                /// arguments
                var arguments = new CmdArguments(args);

                if (arguments.Count() == 0 || arguments.Count() > 2 || !arguments.HasParam(_paramAnchorEntityName))
                {
                    ShowInfo();
                    throw new DependenciesException("arguments error");
                }

                string etn = arguments[_paramAnchorEntityName];
                DependenciesException.ThrowIf(string.IsNullOrEmpty(etn), "etn is null");
                string outfilename = string.Format("dependencies-{0}.dgml", DateTime.Now.ToString("yyyy.MM.dd-hh_mm_ss"));
                if (arguments.Count() > 1 && arguments.HasParam(_paramOutputFileName))
                {
                    outfilename = arguments[_paramOutputFileName];
                }

                /// graph building
                var graph = new BidirectionalGraph<Vertex, Edge<Vertex>>();
                TrackEntity(graph, etn);

                /// saving
                graph.ExportDgml(outfilename);

                Console.WriteLine("completed, {0}...", outfilename);
            }
            catch (Exception e)
            {
                Console.WriteLine();
                Console.WriteLine("====== An error has occured: ");
                Console.WriteLine(e.ToString());
            }

            Console.WriteLine("press <enter> key to exit...");
            Console.ReadLine();
        }

        static void ShowInfo()
        {
            Console.WriteLine();
            Console.WriteLine("You must specify anchor entity name in arguments like this: ");
            Console.WriteLine("> CrmWorkflowDependencies.GraphBuilding.exe --etn salesorder");
        }

        public static void PostProcessing(BidirectionalGraph<Vertex, Edge<Vertex>> graph)
        {
            /// привязать атрибуты к сущностям
            var vps = graph.Vertices.Where(v => v is VertexProperty);
            foreach (VertexProperty vp in vps)
            {
                if (vp.Parent != null && new[] { componenttype.Entity, componenttype.PluginAssembly, componenttype.Workflow }.Contains(vp.Parent.Type)) /// сущности и сборки содержат в себе "детей"
                {
                    /// найти родителя в графе
                    var parent = graph.Vertices.FirstOrDefault(v => v.Equals(vp.Parent));
                    DependenciesException.ThrowIf(parent == null, "VertexProperty parent is null");
                    /// добавить отношение "содержит"
                    graph.AddEdge(new CategoryEdge(parent, "Contains", vp));
                }
            }

            /// атрибуты процесса
            var wfs2 = graph.Vertices.Where(v => v.Type == componenttype.Workflow).ToList();
            foreach (Vertex vwf in wfs2)
            {
                vwf.Properties.Remove("xaml"); /// потому что слишком длинное поле
                foreach (string propKey in vwf.Properties.Keys)
                {
                    /// добавим ноду
                    var vProp = new VertexProperty(vwf, string.Concat(propKey, ": ", vwf.Properties[propKey])) { Type = componenttype.ManagedProperty };
                    vProp = (VertexProperty)graph.AddOrFind(vProp);
                    /// присоедим к БП
                    graph.AddEdge(new CategoryEdge(vwf, "Contains", vProp));
                }
            }
        }

        public static void TrackEntity(BidirectionalGraph<Vertex, Edge<Vertex>> graph, string entityname, params string[] wfnames)
        {
            //var entityVertex = graph.Vertices.FirstOrDefault(v => v.Alias == entityname);
            var entityVertex = graph.AddOrFind(new Vertex(entityname));
            if (entityVertex.Track) return; /// уже обработан
            else entityVertex.Track = true;

            var wfs = crm.RetrieveRegardingWorkflows(entityname);
            
            IEnumerator<Entity> enumWfs;
            if (wfnames.Length == 0)
            {
                enumWfs = wfs.GetEnumerator();
            }
            else
            {
                enumWfs = wfs.Where(wf => wfnames.Contains((string)wf["name"])).GetEnumerator();
            }

            if (wfnames.Length > 0)
            {
                DependenciesException.ThrowIf(!enumWfs.MoveNext(), "не нашелся ни один WF");
            }
            else
            {
                if (!enumWfs.MoveNext()) return;
            }

            //enumWfs.Reset();
            do
            {
                var wf = enumWfs.Current;

                Entity fakeDependency = new Entity();
                fakeDependency["dependentcomponenttype"] = new OptionSetValue((int)componenttype.Workflow);
                fakeDependency["dependentcomponentobjectid"] = wf.Id;

                var current = crm.BuildVertexFrom(fakeDependency, "dependentcomponent");

                graph.AddVertex(current);

                TrackWorkflow(graph, wf, current, 0, 10);
            }
            while (enumWfs.MoveNext());
        }

        /// <summary>
        /// Добавить в граф зависимости текущей вершины
        /// </summary>
        /// <param name="graph"></param>
        /// <param name="wf"></param>
        /// <param name="currentWf"></param>
        public static void TrackWorkflow(BidirectionalGraph<Vertex, Edge<Vertex>> graph, Entity wf, Vertex currentWf, int currentLevel, int maxLevel)
        {
            currentLevel++;
            if (currentLevel > maxLevel) return;

            currentWf = graph.AddOrFind(currentWf);

            if (currentWf.Track) return; /// уже обработан
            else currentWf.Track = true;

            /// Зависимости: 
            var dependencies = crm.RetrieveWorkflowDependencies(wf.Id);

            foreach (var d in dependencies)
            {
                var dVertex = crm.BuildVertexFrom(d, "dependentcomponent"); /// requiredcomponentobjectid
                DependenciesException.ThrowIf(componenttype.Workflow != dVertex.Type, "");
                Guid wfid = (Guid)d.Attributes["dependentcomponentobjectid"];

                DependenciesException.ThrowIf(componenttype.Workflow != dVertex.Type, "зависимости от других WF");

                dVertex = graph.AddOrFind(dVertex);
                graph.AddEdge(new Edge<Vertex>(dVertex, currentWf));

                /// пройтись по связанным бп
                TrackWorkflow(graph, crm.FindRecord("workflow", wfid), dVertex, currentLevel, maxLevel); /// перейти к следующему по иерархии wf
            }

            /// Custom Actions [14.02.04 E.M.]
            VertexProperty[] customActions = currentWf.XamlRetrieveNonMicrosoftAssemblyQN();
            if (customActions != null)
            {
                var ar = (VertexProperty)graph.AddOrFind(new VertexProperty(currentWf, "ActivityReference"));

                foreach (VertexProperty ca in customActions)
                {
                    DependenciesException.ThrowIf(componenttype.PluginType != ca.Type, "customAction type must be PluginType but '{0}'", ca.Type);
                    DependenciesException.ThrowIf(ca.Parent == null, "VertexProperty parent is null");
                    DependenciesException.ThrowIf(componenttype.PluginAssembly != ca.Parent.Type, "customAction parent must be PluginAssembly, but {0}", ca.Parent.Type);

                    /// UPD: [14.02.12 E.M.] make RetrieveDependenciesForDeleteRequest for new Custom Action type                    
                    //var wfAssemblyType = crm.FindRecordByField("plugintype", "typename", ca.Alias);
                    //DependenciesException.ThrowIf(wfAssemblyType == null, "custom workflow activity as plugintype");
                    //var wfAssemblyComponent = crm.RetrieveSolutionComponent(Guid.Empty, wfAssemblyType.Id, componenttype.PluginType);
                    //DependenciesException.ThrowIf(wfAssemblyComponent == null, "custom workflow activity as solutioncomponent");
                    //ca.ID = wfAssemblyType.Id;
                    //if (!graph.ContainsVertex(ca))
                    {
                        /// получим зависимости
                        //var wfActivityDependencies = crm.RetrieveDependenciesForDelete(wfAssemblyComponent.Id, componenttype.PluginAssembly);
                        //var wfActivityDependencies = crm.RetrieveDependenciesForDelete(wfAssemblyType.Id, componenttype.PluginType);
                    }
                    
                    var assembly = graph.AddOrFind(ca.Parent);
                    var type = graph.AddOrFind(ca);

                    graph.AddEdge(new MyEdge(ar, "AssemblyQualifiedName", type));
                }
            }

            /// Необходимые компоненты
            var requirements = crm.RetrieveRequiredComponents(wf.Id);

            string triggerfields = currentWf.Properties.ContainsKey("triggeronupdateattributelist") ? (string)currentWf.Properties["triggeronupdateattributelist"] : null;

            VertexProperty[] updateproperties = currentWf.XamlRetrieveSetEntityProperty();
            VertexProperty[] states = currentWf.XamlRetrieveSetState();

            //var testState = states.FirstOrDefault(s => !(s.Parent is VertexProperty));
            //Assert.IsNull(testState);

            foreach (var r in requirements)
            {
                var rVertex = crm.BuildVertexFrom(r, "requiredcomponent"); /// dependentcomponentobjectid
                rVertex = graph.AddOrFind(rVertex);

                if (rVertex.Type == componenttype.Attribute)
                {
                    /// добавляем атрибуты, изменение которых вызывает БП
                    if (!string.IsNullOrEmpty(triggerfields) && triggerfields.Contains(rVertex.Alias))
                    {
                        graph.AddEdge(new MyEdge(rVertex, "triggeronupdateattributelist", currentWf));
                    }
                    var vp = (VertexProperty)rVertex;
                    DependenciesException.ThrowIf(componenttype.Entity != vp.Parent.Type, "сущность");
                    string etn = vp.Parent.Alias;
                    /// добавляем атрибуты, кот. меняются в БП
                    if (updateproperties.FirstOrDefault(p => p.Alias == rVertex.Alias && p.Parent.Alias == etn) != null)
                    {
                        graph.AddEdge(new MyEdge(currentWf, "SetEntityProperty", rVertex));
                    }
                    /// добавляем setstate
                    if (rVertex.Alias == "statuscode" && states.Length > 0)
                    {
                        /// не здесь, а далее
                    }
                }
                else
                {
                    /// не атрибут
                    if (rVertex.Type != componenttype.Entity) /// с сущностями только чз атрибуты
                    {
                        graph.AddEdge(new MyEdge(currentWf, rVertex.Type.ToString(), rVertex));
                    }
                    else
                    {
                        /// сущность
                        TrackEntity(graph, rVertex.Alias);

                        /// добавляем setstate
                        string etn = rVertex.Alias;
                        if (states.Where(s => s.Parent.Equals(rVertex)).Count() > 0)
                        {
                            /// проверить сущность
                            var statusLabels = crm.GetStatusValueLabels(etn, "statuscode");
                            DependenciesException.ThrowIf(statusLabels.Count == 0, "");
                            foreach (var state in states.Where(s => s.Parent.Equals(rVertex)))
                            {
                                var fsl = statusLabels.First(sl => sl.Key == Convert.ToInt32(state.Alias));

                                var state1 = (VertexProperty)graph.AddOrFind(state);
                                state1.Label = fsl.Value;

                                graph.AddEdge(new MyEdge(currentWf, "SetState.Status", state1));
                                /// связать статус с атрибутом
                                state1.Parent = rVertex;

                                /// найти statsucode
                                var statuscodeAttr = graph.AddOrFind(new VertexProperty(rVertex, "statuscode", "Статус"));

                                /// связать атрибут и значение
                                graph.AddEdge(new MyEdge(statuscodeAttr, "StatusValue", state1));

                                /// добавить линк на атрибут
                                //graph.AddEdge(new MyEdge(currentWf, "SetState", statuscodeAttr));

                                /// и само значение статуса с соотв. сущностью
                                DependenciesException.ThrowIf(componenttype.Entity != state.Parent.Type, "2ой уровень - сущность");
                            }
                        }
                    }
                }

                if (rVertex is VertexProperty)
                {
                    var vp = (VertexProperty)rVertex;
                    if (vp.Parent != null)
                    {
                        var parent = graph.Vertices.FirstOrDefault(v => v.Equals(vp.Parent));
                        if (parent == null)
                        {
                            vp.Parent = graph.AddOrFind(vp.Parent); /// добавить родителя, если еще не был добавлен
                        }
                    }
                }
            }
        }
    }
}